%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% A finite state machine managing the connection from a client to the
%%% event machine (ie instances of process_srv and fieldset_srv).
%%%
%%% Validation of data will be done here. Once passed thru to process_srv or
%%% fieldset_srv the data MUST be clean (ie fieldsets correctly populated
%%% and expected!).
%%%
%%% @headerfile "../include/view.hrl"

-module(view_fsm).

%% A view is a statemachine active on behalf of each client through which the
%% client renders TXM operations and/or processes TXM events.
%% [http://www.erlang.org/doc/man/gen_fsm.html]
-behaviour(gen_fsm).

-include("txm.hrl").
-include("view.hrl").
-include("client.hrl").
-include("process.hrl").

%%% ---------------------------------------------------------------------------
%%% Export client functions
%%% ---------------------------------------------------------------------------

%%% Start and stop functions.
-export([start/1, start/2, start_link/1, start_link/2, stop/1]).

%%% Client functions
-export([attach/2, detach/1, notify/4, reply/4, reply/5]).
-export([current_state/1, get_client_data/1, set_client_data/2]).
-export([handle/3]).

%%% Non-state-related functions
-export([last_used/1]).

%%% gen_fsm behaviour callback functions (private)
-export([init/1, terminate/3]).
-export([handle_event/3, handle_sync_event/4, handle_info/3]).
-export([code_change/4]).

%%% gen_fsm state functions.
-export([?StateIdle/3, ?StateAttached/2, ?StateAttached/3]).

%%% ---------------------------------------------------------------------------
%%% Start and stop and state query functions
%%% ---------------------------------------------------------------------------

%% @doc Starts a view_fsm on the client. If a view_sup is present,
%% then it is used to start the view_fsm, otherwise the calling process
%% is linked.
%% @spec start(module()) -> {ok, pid()}
start(Client) ->
  start(Client, ?UNDEFINED).

%% @spec start(module(), term()) -> {ok, pid()}
start(Client, ClientData) ->
  case whereis(?VIEW_SUP) of
    ?UNDEFINED ->
      start_link(Client, ClientData);
    _ViewSup ->
      supervisor:start_child(?VIEW_SUP, [Client, ClientData])
  end.

%% @doc Starts a view fsm calling back
%% on the supplied client module. Client state is undefined if the
%% one-arg function is called, otherwise an arbitrary term can be
%% supplied.
%% @spec start_link(module()) -> {ok, pid()}
start_link(Client) ->
  start_link(Client, ?UNDEFINED).

%% @spec start_link(module(), term()) -> {ok, pid()}
start_link(Client, ClientData) ->
  gen_fsm:start_link(?MODULE,
                     {?StateIdle,
                      #?VIEW_LOOP_DATA{
                       client = Client,
                       clientData = ClientData}}, []).

%% @doc Stops the specified view fsm by sending a shutdown event.
%% @spec stop(pid()) -> ok
stop(ViewFsm) ->
  gen_fsm:send_all_state_event(ViewFsm, ?STOP).

%% @doc Returns the current FSM state.
%% @spec current_state(pid()) -> atom()
current_state(ViewFsm) ->
  gen_fsm:sync_send_all_state_event(ViewFsm, ?CURRENT_STATE,
                                    ?SYNC_EVENT_TIMEOUT).

%% @doc Returns the user-supplied client data.
%% @spec get_client_data(pid()) -> term()
get_client_data(ViewFsm) ->
  gen_fsm:sync_send_all_state_event(ViewFsm, ?GET_CLIENT_DATA,
                                    ?SYNC_EVENT_TIMEOUT).

%% @doc Sets the user-supplied client data.
%% @spec set_client_data(pid(), term()) -> ok
set_client_data(ViewFsm, Data) ->
  gen_fsm:sync_send_all_state_event(ViewFsm, {?SET_CLIENT_DATA, Data},
                                    ?SYNC_EVENT_TIMEOUT).

%%% ---------------------------------------------------------------------------
%%% Client functions called from the user side.
%%% ---------------------------------------------------------------------------

%% @doc Attaches `ViewPid' to a prog, version and folder with a cookie.
%% Input emissions matching this spec will be passed to the view.
%% @spec attach(pid(), txm_attach_spec()) -> ok
attach(ViewPid, #?TXM_ATTACH_SPEC{} = AttachSpec) ->
  gen_fsm:sync_send_event(ViewPid,
                          {?ATTACH, AttachSpec},
                          ?SYNC_EVENT_TIMEOUT).

%% @doc Detaches `ViewPid' and returns to state_idle.
%% @spec detach(pid()) -> ok
detach(ViewPid) ->
  gen_fsm:sync_send_event(ViewPid,
                          ?DETACH,
                          ?SYNC_EVENT_TIMEOUT).

%% @doc Creates a new event switch process using the supplied field list.
%% Returns the process ID. There's no context required.
notify(ViewPid, OpName, SetName, Proplist) ->
  gen_fsm:sync_send_event(ViewPid,
                          {?NOTIFY, OpName, SetName, Proplist},
                          ?SYNC_EVENT_TIMEOUT).

%% @doc Output from the named op in reply to previously handled input.
%% The reply is in the context of a previously emitted input. The `IsLast'
%% parameter applies only to IRR ops, and is set to `false' for all outputs
%% except the last.
%% Returns ok.
reply(ViewPid, CallRef, SetName, Proplist, IsLast) ->
  gen_fsm:sync_send_event(ViewPid,
                          {?REPLY, CallRef, SetName, Proplist, IsLast},
                          ?SYNC_EVENT_TIMEOUT).

%% @doc As reply/5 but with IsLast set to true.
reply(ViewPid, CallRef, SetName, Proplist) ->
  reply(ViewPid, CallRef, SetName, Proplist, true).

%%% ---------------------------------------------------------------------------
%%% Client functions called from the event switch side.
%%% ---------------------------------------------------------------------------

%% @doc Called back by the machine when sending input to an operation on
%% this view. This is handled asynchronously.
handle(ViewPid, Fid, EventFieldset) ->
  gen_fsm:send_event(ViewPid, {?HANDLE, Fid, EventFieldset}).

%% Returns the time() this `ViewFsm' last handled a client event.
%% @spec last_used(pid()) -> time()
last_used(Pid) ->
  Pid ! {self(), last_used},
  receive
    {Pid, LastUsed} -> LastUsed
  end.

%%% ---------------------------------------------------------------------------
%%% Basic callback functions on gen_fsm.
%%% ---------------------------------------------------------------------------

%% Called back on start.
init({InitialState, LoopData}) ->
  ?INFO([init]),
  process_flag(trap_exit, true),
  ?OK_RETURN(InitialState, LoopData).

%% Called back on stop.
terminate(Reason, StateName, _LoopData) ->
  ?INFO([{terminate, Reason}, {state, StateName}]),
  ?OK_RETURN.

%%% ---------------------------------------------------------------------------
%%% State-specific callback functions on gen_fsm.
%%% ---------------------------------------------------------------------------

%%% ---------------------------------------------------------------------------
%%% state_idle
%%% ---------------------------------------------------------------------------

%% SYNCHRONOUS
%% #1
%% Attach - populates fsm loop data and view cache.
?StateIdle({?ATTACH, #?TXM_ATTACH_SPEC{cookie = Cookie} = AttachSpec},
           _From, LoopData) ->
  ?DEBUG([{?StateIdle, ?ATTACH}]),
  try

    % Resolve the attachment to composition and folder, and get fsm loopdata.
    {CompRef, FolderRef} = resolve_attach_spec(AttachSpec),
    ?OK_RETURN(#?FOLDER{entries = Entries}) = prog_srv:get_record(FolderRef),
    ?OK_RETURN(Contents) = prog_srv:get_shallow_contents(FolderRef),
    {EmitOps, CollectOps} = ops_partition(Contents),

    NewLoopData = LoopData#?VIEW_LOOP_DATA{
                           compRef = CompRef,
                           folderRef = FolderRef,
                           cookie = Cookie,
                           opsNr = Entries,
                           emitOps = EmitOps,
                           collectOps = CollectOps},

    % Activate this view so that fieldset_srv processes can match it
    % against input emissions.
    view_db:activate(self(), CompRef, Cookie, dict:fetch_keys(EmitOps)),

    % Move to state_attached.
    {reply, ?OK_RETURN, ?StateAttached, timestamp(NewLoopData)}
  catch
    error:Reason ->
      ?ERROR([{?StateIdle, Reason}]),
      {reply, ?ERROR_RETURN(Reason), ?StateIdle, timestamp(LoopData)}
    end.

%%% ---------------------------------------------------------------------------
%%% state_attached
%%% ---------------------------------------------------------------------------

%% SYNCHRONOUS
%% #1
%% Notify starts a new process, generating the event fieldset from the
%% supplied parameters. There is no context apart from the view's cookie,
%% since the notification is not in response to any input.
?StateAttached({?NOTIFY, OpName, SetName, Proplist},
               _From,
               #?VIEW_LOOP_DATA{compRef = CompRef,
                                opsNr = OpsNr,
                                collectOps = CollectOps,
                                cookie = Cookie} = LoopData) ->
  ?DEBUG([{?StateAttached, ?NOTIFY}]),
  try

    % Initialize a new fieldset_srv to handle the notification.
    OpRef = nr_set:ref_for_name(OpName, OpsNr),
    #?OP{opType = ?OP_TYPE_N,
              outputs = Outputs} = dict:fetch(OpRef, CollectOps),
    Output = nr_set_dict:get_nr_set(SetName, Outputs),
    Fields = nr_set:to_proplist_by_ref(Proplist, Output),
    {ok, Fid} = fieldset_srv:start_link(CompRef, Cookie, Fields),
    fieldset_srv:activate(Fid),

    % Remain in state_attached.
    {reply, ?OK_RETURN, ?StateAttached, timestamp(LoopData)}
  catch
    error:Reason ->
      ?ERROR([{?StateAttached, Reason}]),
      {reply, ?ERROR_RETURN(Reason), ?StateAttached, timestamp(LoopData)}
    end;

%% #3
%% Reply builds and sends an event fieldset back to the fieldset_srv.
?StateAttached({?REPLY, CallRef, SetName, Proplist, IsLast},
               _From,
               #?VIEW_LOOP_DATA{compRef = CompRef,
                                collectOps = CollectOps,
                                pendingReplies = PendingReplies} = LoopData) ->
  ?DEBUG([{?StateAttached, ?REPLY}]),
  try

    % Lookup the expected reply by reference.
    #?REPLY_RECORD{callRef = CallRef,
                   opRef = OpRef,
                   fid = Fid,
                   cookie = Cookie} = dict:fetch(CallRef, PendingReplies),

    % Build and send the reply to fieldset_srv.
    #?OP{outputs = Outputs,
         opType = OpType} = dict:fetch(OpRef, CollectOps),
    Output = nr_set_dict:get_nr_set(SetName, Outputs),
    Fields = nr_set:to_proplist_by_ref(Proplist, Output),

    case OpType of
      ?OP_TYPE_RR ->
        EventFieldset = #?EVENT_FIELDSET{
                     opRef = OpRef,
                     setName = SetName,
                     fields = Fields,
                     isLast = IsLast},
        fieldset_srv:collect(Fid, EventFieldset);
      ?OP_TYPE_IRR ->
        {ok, NewFid} = fieldset_srv:start_link(CompRef, Cookie, Fields),
        fieldset_srv:activate(NewFid)
    end,

    % The IsLast condition only applies to IRR ops at this time.
    NewLoopData =
      case {OpType, IsLast} of
        {?OP_TYPE_IRR, false} ->
          LoopData;
        _Others ->
          LoopData#?VIEW_LOOP_DATA{pendingReplies =
                                     dict:erase(CallRef, PendingReplies)}
      end,
    {reply, ?OK_RETURN, ?StateAttached, timestamp(NewLoopData)}
  catch
    error:Reason ->
      ?ERROR([{?StateAttached, Reason}]),
      {reply, ?ERROR_RETURN(Reason), ?StateAttached, timestamp(LoopData)}
  end;

%% #2
%% Detach clears the folder reference and cookie and moves to state idle.
?StateAttached(?DETACH, _From,
               #?VIEW_LOOP_DATA{client = Client}) ->
  ?DEBUG([{?StateAttached, ?DETACH}]),

  % Wipe the view cache entries on this view_fsm.
  view_db:deactivate(self()),

  % Clear loop data fields that were set upon attachment.
  NewLoopData = #?VIEW_LOOP_DATA{client = Client},

  % Move to state_idle.
  {reply, ?OK_RETURN, ?StateIdle, timestamp(NewLoopData)}.

%% ASYNCHRONOUS
%% #1
%% Input data arriving from the fieldset_srv process is handled asynchronously
%% and we directly callback the view_fsm's client module.
?StateAttached({?HANDLE, Fid,
                #?EVENT_FIELDSET{opRef = OpRef,
                                 cookie = Cookie,
                                 fields = FieldsByRef}},
               #?VIEW_LOOP_DATA{client = Client,
                                opsNr = OpsNr,
                                emitOps = EmitOps,
                                pendingReplies = PendingReplies} = LoopData) ->
  ?DEBUG([{?StateAttached, ?HANDLE}]),

  % It may be better to use ETS for the pending replies table but I'd expect
  % a small number of pending replies at any one time.
  CallRef = make_ref(),
  NewPendingReplies =
    % If the op has output, update our pending replies.
    case dict:fetch(OpRef, EmitOps) of
      Op when Op#?OP.opType == ?OP_TYPE_RR orelse
                   Op#?OP.opType == ?OP_TYPE_IRR ->
        ReplyRecord = #?REPLY_RECORD{
                       callRef = CallRef,
                       opRef = OpRef,
                       fid = Fid,
                       cookie = Cookie},
        dict:store(CallRef, ReplyRecord, PendingReplies);
      _Otherwise ->
        PendingReplies
      end,

  % Spawn a new process to run the client code. If there's output this will
  % eventually call back on view_fsm:reply/4. There's no race condition since
  % this function must finish with its new loop data before the reply call
  % can be handled.
  OpName = nr_set:name_for_ref(OpRef, OpsNr),
  #?OP{input = Input} = dict:fetch(OpRef, EmitOps),
  FieldsByName = nr_set:to_proplist_by_name(FieldsByRef, Input),

  % The client handler process death will result in a call to our
  % handle_info/3 function with an {'EXIT', Pid, Reason} tuple.
  spawn_link(Client, handle_input, [self(), CallRef, OpName, FieldsByName]),

  NewLoopData = LoopData#?VIEW_LOOP_DATA{
                         pendingReplies = NewPendingReplies},
  {next_state, ?StateAttached, timestamp(NewLoopData)}.

%%% ---------------------------------------------------------------------------
%%% All-state callback functions on gen_fsm.
%%% ---------------------------------------------------------------------------

%% Handles all-state async shutdown event by returning stop tuple, which causes
%% gen_fsm to shutdown.
handle_event(?STOP, _CurrentState, LoopData) ->
  {stop, normal, LoopData}.

%% Handles all-state sync request for current state name.
handle_sync_event(?CURRENT_STATE, _From, CurrentState, LoopData) ->
  ?DEBUG([{?CURRENT_STATE, CurrentState}]),
  {reply, CurrentState, CurrentState, LoopData};

%% Handles all-state sync request for client-specific data.
handle_sync_event(?GET_CLIENT_DATA, _From, CurrentState, LoopData) ->
  ?DEBUG([?GET_CLIENT_DATA]),
  {reply, LoopData#?VIEW_LOOP_DATA.clientData, CurrentState, LoopData};

%% Handles all-state sync request to set client-specific data.
handle_sync_event({?SET_CLIENT_DATA, NewData}, _From, CurrentState, LoopData) ->
  ?DEBUG([?SET_CLIENT_DATA]),
  {reply, ?OK_RETURN, CurrentState,
   LoopData#?VIEW_LOOP_DATA{clientData = NewData}}.

%% Handles out-of-band info requests unrelated to state, including the
%% supervisor requesting last used time for purposes of determining when
%% to shutdown inactive client_fsm instances.
%% Note that this request, like several others, does not update the
%% lastUsed state field.
handle_info({Pid, last_used},CurrentState,
            #?VIEW_LOOP_DATA{lastUsed = LastUsed} = LoopData) ->
  ?DEBUG([{handle_info, last_used}]),
  Pid ! {self(), LastUsed},
  {next_state, CurrentState, LoopData};

%% Handles death of Client:handle_input/4 process instances which are linked
%% to this process.
handle_info({'EXIT', _Client, _Reason}, CurrentState, LoopData) ->
  {next_state, CurrentState, LoopData}.

%%% --------------------------------------------------------------------------
%%% Unimplemented behaviour callbacks.
%%% --------------------------------------------------------------------------
code_change(_, _, _, _) -> throw(?REASON_NOT_IMPLEMENTED).

%%% --------------------------------------------------------------------------
%%% Utility functions
%%% --------------------------------------------------------------------------

%% @doc Update the lastUsed field.
%% @spec timestamp(#loop_data{}) -> #loop_data{}
timestamp(#?VIEW_LOOP_DATA{} = LoopData) ->
  LoopData#?VIEW_LOOP_DATA{lastUsed = now()}.

%% @doc Returns a tuple of {EmitOps, CollectOps}, each element being a dict
%% of ops keyed by ref. EmitOps are those with input, CollectOps are those
%% with output (RR and IRR ops are in both sets).
%% @spec ops_partition(list()) -> {dict(), dict()}
ops_partition(Contents) ->
  lists:foldl(fun(#?OP{opType = OpType} = Op, {EmitOps, CollectOps}) ->
                   OpRef = prog_srv:record_to_ref(Op),
                   case OpType of
                     ?OP_TYPE_N ->
                       {EmitOps,
                        dict:store(OpRef, Op, CollectOps)};
                     ?OP_TYPE_OO ->
                       {dict:store(OpRef, Op, EmitOps),
                        CollectOps};
                     ?OP_TYPE_RR ->
                       {dict:store(OpRef, Op, EmitOps),
                        dict:store(OpRef, Op, CollectOps)};
                     ?OP_TYPE_IRR ->
                       {dict:store(OpRef, Op, EmitOps),
                        dict:store(OpRef, Op, CollectOps)};
                     ?OP_TYPE_NULL ->
                       {EmitOps, CollectOps}
                     end;
                (_Other, Acc) ->
                  Acc
              end, {dict:new(), dict:new()}, Contents).

%% @doc Returns the composition ref and the subfolder ref (possibly the same)
%% by resolving `AttachSpec'.
%% @spec resolve_attach_spec(txm_attach_spec()) -> {ref(), ref()}
resolve_attach_spec(#?TXM_ATTACH_SPEC{prog = Prog,
                                      version = Version,
                                      folder = Folder}) ->
  {ok, ProgRef} = prog_srv:resolve_absolute(Prog),
  {ok, #?OP{rootFolderRef = RootFolderRef,
            versions = Versions}} = prog_srv:get_record(ProgRef),

  % Get the composition reference (a folder ref) using the op version spec.
  CompRef =
    case Version of
      none ->
        RootFolderRef;
      latest ->
         get_latest_version(Versions);
      dynamic ->
        get_latest_version(Versions);
      Vsn when is_integer(Vsn) ->
        get_version_by_number(Vsn, Versions);
      Label when is_list(Label) ->
        get_version_by_label(Label, Versions)
    end,

  % Get the folder within the specified composition and return both refs.
  {ok, FolderRef} = prog_srv:resolve_relative(CompRef, Folder),
  {CompRef, FolderRef}.

%% @doc Returns root folder reference for latest version in the version list.
%% @spec get_latest_version(list()) -> ref()
get_latest_version([{_Vsn, _Label, LatestRef} | _Older]) ->
  LatestRef.

%% @doc Returns root folder reference for version `Vsn' in the version list.
%% @spec get_version_by_number(integer(), list()) -> ref()
get_version_by_number(Vsn, [{Vsn, _Label, Ref} | _Rest]) ->
  Ref;

%% #2
get_version_by_number(Vsn, [_Wrong | Rest]) ->
  get_version_by_number(Vsn, Rest).

%% @doc Returns root folder reference for version labelled `Label' in the
%% version list.
%% @spec get_version_by_label(string(), list()) -> ref()
get_version_by_label(Label, [{_Vsn, Label, Ref} | _Rest]) ->
  Ref;

%% #2
get_version_by_label(Label, [_Wrong | Rest]) ->
  get_version_by_label(Label, Rest).
